package com.atguigu.gulimall.product.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.alibaba.nacos.common.util.UuidUtils;
import com.atguigu.gulimall.product.config.MyRedissonConfig;
import com.atguigu.gulimall.product.service.CategoryBrandRelationService;
import com.atguigu.gulimall.product.vo.Catelog2Vo;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.atguigu.common.utils.PageUtils;
import com.atguigu.common.utils.Query;

import com.atguigu.gulimall.product.dao.CategoryDao;
import com.atguigu.gulimall.product.entity.CategoryEntity;
import com.atguigu.gulimall.product.service.CategoryService;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;


@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {

//    @Autowired
//    CategoryDao categoryDao;

    @Autowired
    CategoryBrandRelationService categoryBrandRelationService;
    @Autowired
    StringRedisTemplate stringRedisTemplate;
    @Autowired
    MyRedissonConfig redisson;

    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        IPage<CategoryEntity> page = this.page(
                new Query<CategoryEntity>().getPage(params),
                new QueryWrapper<CategoryEntity>()
        );

        return new PageUtils(page);
    }

    @Override
    public List<CategoryEntity> listWithTree() {
        //1、查出所有分类
        List<CategoryEntity> entities = baseMapper.selectList(null);

        //2、组装成父子的树形结构

        //2.1）、找到所有的一级分类
        List<CategoryEntity> level1Menus = entities.stream().filter(categoryEntity ->
             categoryEntity.getParentCid() == 0
        ).map((menu)->{
            menu.setChildren(getChildrens(menu,entities));
            return menu;
        }).sorted((menu1,menu2)->{
            return (menu1.getSort()==null?0:menu1.getSort()) - (menu2.getSort()==null?0:menu2.getSort());
        }).collect(Collectors.toList());




        return level1Menus;
    }

    @Override
    public void removeMenuByIds(List<Long> asList) {
        //TODO  1、检查当前删除的菜单，是否被别的地方引用

        //逻辑删除
        baseMapper.deleteBatchIds(asList);
    }

    //[2,25,225]
    @Override
    public Long[] findCatelogPath(Long catelogId) {
        List<Long> paths = new ArrayList<>();
        List<Long> parentPath = findParentPath(catelogId, paths);

        Collections.reverse(parentPath);


        return parentPath.toArray(new Long[parentPath.size()]);
    }

    /**
     * 级联更新所有关联的数据
     * @CacheEvict:失效模式
     * 1、同时进行多种缓存操作  @Caching
     * @Caching(evict = {
     *     @CacheEvict(value = "catgory", key="'getLevel1Categorys'"),
     *     @CacheEvict(value = "catgory", key="'getCagelogJson'")
     * }
     *       在@Cache操作两遍
     * 2、指定删除某个分区下的所有数据 @CacheEvict(value = "catgory", allEntries = true)，true时把catgory分区下的所有缓存都删除
     * 3、存储同一类型的数据，都可以指定同一个分区。分区的默认就是缓存的前缀
     *
     * @param category
     */
    @CacheEvict(value = "category", allEntries = true)
    @Transactional
    @Override
    public void updateCascade(CategoryEntity category) {
        this.updateById(category);
        categoryBrandRelationService.updateCategory(category.getCatId(),category.getName());
    }

    /**
     * TODO 产生对外内存溢出： OutOfDirectMemoryError
     * 1）、springboot2 以后默认使用lettuce作为操作redis的客户端。它使用netty进行网络通信
     * 2）、lettuce的bug导致netty 堆外内存溢出 -Xmx100m;
     *      可以通过 -Dio.netty.maxDirectMemory进行设置
     *      一般不使用 -Dio。。。 的方式
     *
     * 解决方法：1）、升级lettuce
     * 2）、使用jedis
     * @return
     */


    public Map<String, List<Catelog2Vo>> getCatelogJsonWithRedisLock() {
        String uuid = UUID.randomUUID().toString();
        Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS);
        if(lock){
            //加锁成功，执行业务
            Map<String, List<Catelog2Vo>> catelogJson = null;
            //数据获取成功，删除锁
            //先查lock的值是否是自己的uuid，如果是则删除
            //如果删除是分开的，会造成查的是自己的，准备删时锁过期了，别的线程加锁，然后删除锁，实际上删除的是别人的锁
            //所以查询和删除放在rula脚本中，保证原子性
            try {
                //业务超过锁时间，删除锁
                catelogJson = getCatelogJson();
            } finally {
                String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
                stringRedisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);
            }

            return catelogJson;
        }else {
            //加锁失败，继续获取锁
            return getCatelogJsonWithRedisLock();
        }
    }

    @Cacheable(value = {"category"}, key = "#root.methodName")
    @Override
    public Map<String, List<Catelog2Vo>> getCatelogJson() {
        Map<String, List<Catelog2Vo>> catelogJsonFromDb = getCatelogJsonFromDb();
        return catelogJsonFromDb;
    }

//    @Override
    public Map<String, List<Catelog2Vo>> getCatelogJson2() {
        /**
         * 1）、缓存穿透，指在大量访问在缓存中不存在的数据时，导致到db中去查询
         * 2）、缓存击穿，指访问某一热点缓存时，如：正好过期等问题，导致该数据的缓存失效，然后去访问db
         * 3）、缓存雪崩，指大面积的缓存失效，导致访问缓存的请求全部去访问db
         *
         * 解决：1）、空结果缓存：解决缓存穿透
         * 2）、设置过期时间（加随机值）：解决缓存雪崩
         * 3）、加锁：解决缓存击穿
         */
        ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
        String catelogJSON = ops.get("catelogJSON");
        if(StringUtils.isEmpty(catelogJSON)){
            Map<String, List<Catelog2Vo>> catelogJsonFromDb = getCatelogJsonFromDb();
            String s = JSON.toJSONString(catelogJsonFromDb);
            ops.set("catelogJSON", s);
            return catelogJsonFromDb;
        }

        Map<String, List<Catelog2Vo>> stringListMap = JSON.parseObject(catelogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {
        });
        return stringListMap;
    }

    public Map<String, List<Catelog2Vo>> getCatelogJsonFromDb() {
        //优化sql查询
        List<CategoryEntity> selectList = baseMapper.selectList(null);

        List<CategoryEntity> level1Categorys = getParent_cid(selectList, 0L);
        Map<String, List<Catelog2Vo>> cate2Result = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
            List<CategoryEntity> l2List = getParent_cid(selectList, v.getCatId());

            List<Catelog2Vo> catelog2VoList = null;
            if (l2List != null) {
                catelog2VoList = l2List.stream().map(l2 -> {
                    Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());

                    List<CategoryEntity> l3List = getParent_cid(selectList, l2.getCatId());
                    List<Catelog2Vo.Catelog3Vo> catelog3VoList = l3List.stream().map(l3 -> {
                        Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
                        return catelog3Vo;
                    }).collect(Collectors.toList());
                    catelog2Vo.setCatalog3List(catelog3VoList);

                    return catelog2Vo;
                }).collect(Collectors.toList());
            }
            return catelog2VoList;
        }));

        return cate2Result;
    }

    private List<CategoryEntity> getParent_cid(List<CategoryEntity> selectList, Long parent_cid) {
        List<CategoryEntity> collect = selectList.stream().filter(item -> parent_cid == item.getParentCid()).collect(Collectors.toList());
//        return baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", v.getCatId()));
        return collect;
    }

    /**
     * 1、每个需要缓存的数据我们都来指定放入哪个名字的缓存中。【缓存的分区（按照业务类型分）】
     * 2、@Cacheable({"catgory"})
     *      代表当前方法使用缓存，如果有缓存，方法不用调用。如果缓存中没有数据，会调用方法把返回结果存入到缓存中
     * 3、默认行为：
     *      1）、如果缓存中有，方法不调用
     *      2）、key默认自动生成：缓存的名字(分区的名字)::SimpleKey []
     *      3）、缓存的value值默认使用jdk序列化机制，序列化后存入到redis中
     *      4）、默认时间ttl为：-1
     *
     *   自定义：
     *      1）、指定生成缓存的key   key属性指定名字，key="#root.method.name"以方法名作为key
     *      2）、指定缓存的存活时间    配置文件中修改ttl,spring.cache.redis.time-to-live=3600000
     *      3）、将数据保存为json数据
     *
     * 4、Spring-Cache 的不足
     *      1）、读模式：
     *          缓存穿透：查询一个null数据。解决：缓存空数据；配置spring.cache.redis.cache-null-values=true
     *          缓存击穿：大并发进来同时查询一个刚好过期的数据：解决：加锁；默认是无加锁的，注解加sync = true
     *          缓存雪崩：大量的key同时过期。解决：加随机过期时间，spring.cache.redis.time-to-live=3600000
     *      2)、写模式：（缓存与数据库一致）
     *          1）、读写加锁
     *          2）、引入Canal，感知Mysql的更新去更新数据
     *          3）、读多写多，直接去数据库查询即可
     *      总结：
     *          常规数据：（读多写少，即时性，一致性要求不高的数据）；完全可以使用Spring-Cache，设置一个过期时间即可
     *          特殊数据：特殊设计
     *
     * 原理：
     *      CacheManager(RedisCacheManager) -> Cache(RedisCache) -> Cache负责数据的读写
     * @return
     */
    //
    @Cacheable(value = {"category"}, key="#root.method.name", sync = true)
    @Override
    public List<CategoryEntity> getLevel1Categorys() {
        List<CategoryEntity> entities = this.baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
        return entities;
    }

    //225,25,2
    private List<Long> findParentPath(Long catelogId,List<Long> paths){
        //1、收集当前节点id
        paths.add(catelogId);
        CategoryEntity byId = this.getById(catelogId);
        if(byId.getParentCid()!=0){
            findParentPath(byId.getParentCid(),paths);
        }
        return paths;

    }


    //递归查找所有菜单的子菜单
    private List<CategoryEntity> getChildrens(CategoryEntity root,List<CategoryEntity> all){

        List<CategoryEntity> children = all.stream().filter(categoryEntity -> {
            return categoryEntity.getParentCid() == root.getCatId();
        }).map(categoryEntity -> {
            //1、找到子菜单
            categoryEntity.setChildren(getChildrens(categoryEntity,all));
            return categoryEntity;
        }).sorted((menu1,menu2)->{
            //2、菜单的排序
            return (menu1.getSort()==null?0:menu1.getSort()) - (menu2.getSort()==null?0:menu2.getSort());
        }).collect(Collectors.toList());

        return children;
    }



}